﻿using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace TimeTravel.XnaLibrary.UI
{
	/// <summary>
	/// Represents a stack panel used for hosting child elements.
	/// </summary>
	public class StackPanel : Control, IPanel
	{
		private Dictionary<string, Control> mChildren;
		private Dictionary<string, Control> mVisibleChildren;
		private Dictionary<string, Rectangle> mRelativeChildrenLocations;
		private int mVisibilityPosition;
		private int mOffset;
		private int mActualContentLength;
		private Rectangle mVisibleContent = Rectangle.Empty;
		private StackPanelOrientation mOrientation;

		/// <summary>
		/// Returns a dictionary containing all of the stackpanel`s visible children.
		/// </summary>
		public Dictionary<string, Control> VisibleChildren
		{
			get { return mVisibleChildren; }
		}

		/// <summary>
		/// Gets or sets the position after which content should is visible.
		/// </summary>
		public int VisibilityPosition
		{
			get { return mVisibilityPosition; }
			set
			{
				if (value < 0)
					mVisibilityPosition = 0;
				else
					mVisibilityPosition = value;
			}
		}

		/// <summary>
		/// Gets or sets the offset between child elements.
		/// </summary>
		public int Offset
		{
			get { return mOffset; }
			set { mOffset = value; }
		}

		/// <summary>
		/// Gets or sets the stackpanel`s orientation.
		/// </summary>
		public StackPanelOrientation Orientation
		{
			get { return mOrientation; }
		}

		/// <summary>
		/// Gets or sets a value indicating if this stack panel should raise events.
		/// </summary>
		public override bool IsEnabled
		{
			get
			{
				return base.IsEnabled;
			}
			set
			{
				base.IsEnabled = value;
				foreach (var namedControl in mChildren)
				{
					namedControl.Value.IsEnabled = value;
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating if this stack panel should be drawn.
		/// </summary>
		public override bool IsVisible
		{
			get
			{
				return base.IsVisible;
			}
			set
			{
				base.IsVisible = value;
				foreach (var namedControl in mChildren)
				{
					namedControl.Value.IsVisible = value;
				}
			}
		}

		/// <summary>
		/// Initializes a new StackPanel instance with specified orientation.
		/// </summary>
		/// <param name="orientation">The orientation of the panel.</param>
		public StackPanel(StackPanelOrientation orientation)
		{
			mChildren = new Dictionary<string, Control>();
			mVisibleChildren = new Dictionary<string, Control>();
			mRelativeChildrenLocations = new Dictionary<string, Rectangle>();
			mOrientation = orientation;
		}

		/// <summary>
		/// Calculates the StackPanel`s currently visible children.
		/// </summary>
		public void CalculateVisibleContent()
		{
			mVisibleContent = Location;
			if (mOrientation == StackPanelOrientation.Vertical)
			{
				mVisibleContent.X = 0;
				mVisibleContent.Y = mVisibilityPosition;
			}
			else
			{
				mVisibleContent.X = mVisibilityPosition;
				mVisibleContent.Y = 0;
			}
			mVisibleChildren.Clear();
			foreach (var child in mChildren)
			{
				if (mVisibleContent.Intersects(mRelativeChildrenLocations[child.Key]))
				{
					mVisibleChildren.Add(child.Key, child.Value);
					child.Value.Location = mRelativeChildrenLocations[child.Key];
					child.Value.Location.Offset(this.Location.Location);
					if (mOrientation == StackPanelOrientation.Vertical)
					{
						child.Value.Location.Y -= mVisibleContent.Y;
					}
					else
					{
						child.Value.Location.X -= mVisibleContent.X;
					}
					if (child.Value is Canvas)
					{
						(child.Value as Canvas).UpdateLocation();
					}
				}
			}
		}

		/// <summary>
		/// Adds a child control to the StackPanel.
		/// </summary>
		/// <param name="control">The control to add.</param>
		public void AddChild(Control control)
		{
			Rectangle rect = control.Location;
			if (mOrientation == StackPanelOrientation.Vertical)
			{
				rect.Y = mActualContentLength + Offset;
				rect.X = 0;
				mActualContentLength += Offset + control.Location.Height;
			}
			else
			{
				rect.X = mActualContentLength + Offset;
				rect.Y = 0;
				mActualContentLength += Offset + control.Location.Width;
			}
			mChildren.Add(control.Name, control);
			mRelativeChildrenLocations.Add(control.Name, rect);
		}

		/// <summary>
		/// Removes the child control with the specified name.
		/// </summary>
		/// <param name="controlName">The name of the control to be removed.</param>
		public void RemoveChild(string controlName)
		{
			List<Control> toBeRepositioned;
			Control toBeRemoved = mChildren[controlName];
			if (mOrientation == StackPanelOrientation.Vertical)
			{
				toBeRepositioned = new List<Control>(mChildren.Values.Where(control => control.Location.Y > toBeRemoved.Location.Y));
				foreach (var control in toBeRepositioned)
				{
					Rectangle relative = mRelativeChildrenLocations[control.Name];
					relative.Y -= mRelativeChildrenLocations[controlName].Height - mRelativeChildrenLocations[controlName].Y + Offset;
					mRelativeChildrenLocations[control.Name] = relative;
				}
			}
			else
			{
				toBeRepositioned = new List<Control>(mChildren.Values.Where(control => control.Location.X > toBeRemoved.Location.X));
				foreach (var control in toBeRepositioned)
				{
					Rectangle relative = mRelativeChildrenLocations[control.Name];
					relative.X -= mRelativeChildrenLocations[controlName].Width - mRelativeChildrenLocations[controlName].X + Offset;
					mRelativeChildrenLocations[control.Name] = relative;
				}
			}
			mChildren.Remove(controlName);
			mVisibleChildren.Remove(controlName);
			mRelativeChildrenLocations.Remove(controlName);
		}

		/// <summary>
		/// Removes all children controls which are placed after the specified control.
		/// </summary>
		/// <param name="controlName">The name of the control to remove children after.</param>
		public void RemoveAfterChild(string controlName)
		{
			Rectangle controlLocation = mRelativeChildrenLocations[controlName];
			IEnumerable<string> toBeRemoved;
			if (mOrientation == StackPanelOrientation.Vertical)
			{
				toBeRemoved = mChildren.Keys.Where(name => mRelativeChildrenLocations[name].Y > controlLocation.Y);	
			}
			else
			{
				toBeRemoved = mChildren.Keys.Where(name => mRelativeChildrenLocations[name].X > controlLocation.X);				
			}

			foreach (var name in toBeRemoved.ToArray())
			{
				mChildren.Remove(name);
				mVisibleChildren.Remove(name);
				mRelativeChildrenLocations.Remove(name);
			}

		}

		/// <summary>
		/// Gets the child control with the specified name.
		/// </summary>
		/// <param name="controlName">The control`s name.</param>
		public Control GetChild(string controlName)
		{
			return mChildren[controlName];
		}

		/// <summary>
		/// Adds the specified offset to the child`s relative position.
		/// </summary>
		/// <param name="controlName">The name of the control to move.</param>
		/// <param name="offset">The offset to move the child control with.</param>
		public void MoveChild(string controlName, Point point)
		{
			Rectangle relative = mRelativeChildrenLocations[controlName];
			relative.Offset(point);
			if (relative.X < 0)
				relative.X = 0;
			if (relative.Y < 0)
				relative.Y = 0;
			mRelativeChildrenLocations[controlName] = relative;
		}

		/// <summary>
		/// Renames the specified child.
		/// </summary>
		/// <param name="controlName">The name of the control to be removed.</param>
		/// <param name="newName">The new name of the control.</param>
		public void RenameChild(string controlName, string newName)
		{
			Control child = mChildren[controlName];
			child.Name = newName;
			mChildren.Remove(controlName);
			mChildren.Add(newName, child);
			Rectangle relativeLocation = mRelativeChildrenLocations[controlName];
			mRelativeChildrenLocations.Remove(controlName);
			mRelativeChildrenLocations.Add(newName, relativeLocation);
			if (mVisibleChildren.ContainsKey(controlName))
			{
				mVisibleChildren.Remove(controlName);
				mVisibleChildren.Add(newName, child);
			}

		}

		/// <summary>
		/// Removes all children.
		/// </summary>
		public void Clear()
		{
			mChildren.Clear();
			mVisibleChildren.Clear();
			mRelativeChildrenLocations.Clear();
			mActualContentLength = 0;
			mVisibilityPosition = 0;
		}

		/// <summary>
		/// Checks for and raises any events that happened on the stack panel or any of its child elements.
		/// </summary>
		/// <param name="currentState">The current mouse state.</param>
		/// <param name="previousState">The mouse state in the previous frame.</param>
		public override void CheckForEvents(MouseState currentState, MouseState previousState)
		{
			foreach (var child in mChildren)
			{
				child.Value.CheckForEvents(currentState, previousState);
			}
			base.CheckForEvents(currentState, previousState);
		}

		/// <summary> 
		/// Draws the stack panel and all of its child elements with the specified SpriteBatch. SpriteBatch.Begin must have been called prior to calling this method.
		/// </summary>
		/// <param name="spriteBatch">The sprite batch to draw with.</param>
		public override void Draw(SpriteBatch spriteBatch)
		{
			foreach (var child in mVisibleChildren)
			{
				child.Value.Draw(spriteBatch);
			}
		}
		
		/// <summary>
		/// Reorders the children of the stack panel.
		/// </summary>
		public void Reorder()
		{
			mActualContentLength = 0;
			List<Control> controls = new List<Control>(mChildren.Values);
			if (mOrientation == StackPanelOrientation.Vertical)
				controls.Sort(CompareChildrenVertical);
			else
				controls.Sort(CompareChildrenHorizontal);

			mChildren.Clear();
			mRelativeChildrenLocations.Clear();
			foreach (var control in controls)
			{
				AddChild(control);
			}
		}

		private int CompareChildrenVertical(Control child1, Control child2)
		{
			Rectangle rect1 = mRelativeChildrenLocations[child1.Name],
				rect2 = mRelativeChildrenLocations[child2.Name];
			if (rect1.Y > rect2.Y)
				return 1;
			else if (rect1.Y < rect2.Y)
				return -1;
			else
				return 0;
		}

		private int CompareChildrenHorizontal(Control child1, Control child2)
		{
			Rectangle rect1 = mRelativeChildrenLocations[child1.Name],
				rect2 = mRelativeChildrenLocations[child2.Name];
			if (rect1.X > rect2.X)
				return 1;
			else if (rect1.X < rect2.X)
				return -1;
			else
				return 0;
		}
	}

	/// <summary>
	/// Specified the orientation of a stack panel.
	/// </summary>
	public enum StackPanelOrientation
	{
		Vertical, Horizontal
	}
}
